package org.giorgiocalderolla.showrssx;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.TimerTask;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.giorgiocalderolla.showrssx.Configuration.Status;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

/**
 * @author Giorgio Calderolla
 * 
 *         This TimerTask checks the feed and downloads new torrents
 */
public class FeedChecker extends TimerTask {

	private Configuration configuration = null;

	/**
	 * Constructor, takes the feed url as the only parameter
	 * 
	 * @param url
	 *            url of the feed to check
	 */
	public FeedChecker(Configuration configuration) {
		this.configuration = configuration;
	}

	/**
	 * This method will be called periodically to check the feed and download
	 * every new torrent file available
	 */
	@Override
	public synchronized void run() {

		configuration.setCurrentStatus(Status.ACTIVE_WORKING);

		// fetch torrent list
		ArrayList<String> torrents;
		try {
			torrents = downloadTorrentList(configuration.getFeedUrl());
		} catch (IOException e) {
			// probably there's no Internet connection available. this is to be
			// expected at times, so we just return
			configuration.setLastUpdateFailed(true);
			configuration
					.addProblem(new Problem(
							"Couldn't download the torrent list. You're probably not connected to the Internet",
							""));
			configuration.setCurrentStatus(Status.ACTIVE_IDLE);
			return;
		}

		boolean failed = false;

		// download every new file on the list
		for (String torrent : torrents) {
			if (configuration.getDownloadedTorrents().contains(torrent) == false) {
				try {
					downloadTorrent(new URL(torrent));
				} catch (IOException e) {
					// again, probably no Internet, we'll skip this one
					configuration
							.addProblem(new Problem(
									"Couldn't download torrent \""
											+ torrent
											+ "\". You're probably not connected to the Internet",
									""));
					failed = true;
					continue;
				}
			}
			configuration.addDownloadedTorrent(torrent);
		}

		// notify the configuration of the update
		Calendar calendar = Calendar.getInstance();
		SimpleDateFormat dateFormat = new SimpleDateFormat("h:mm a");
		configuration.setLastUpdateTime(dateFormat.format(calendar.getTime()));
		configuration.setLastUpdateFailed(failed);

		configuration.setCurrentStatus(Status.ACTIVE_IDLE);
	}

	/**
	 * Utility method to construct a valid and useful filename from a URL, for
	 * example to download a file
	 * 
	 * @param url
	 *            the URL we want a filename from
	 * @return a filename string, with the appropriate extension
	 * @throws IOException
	 *             if the url-decoding fails
	 */
	public static String urlToFilename(URL url) throws IOException {

		String filename = url.getFile().substring(
				url.getFile().lastIndexOf("/") + 1);

		filename = URLDecoder.decode(filename, "UTF-8");

		// add a .torrent extension if not present
		if (!filename.endsWith(".torrent")) {
			filename += ".torrent";
		}

		return filename;
	}

	/**
	 * Downloads a single .torrent file given its url
	 * 
	 * @param url
	 *            the url where the .torrent file to download is located
	 * @throws IOException
	 *             when the download fails, often because of lack of Internet
	 *             connectivity
	 */
	private void downloadTorrent(URL url) throws IOException {

		// extract the filename
		String filename = urlToFilename(url);

		// download and save the file
		BufferedInputStream inputStream = null;
		FileOutputStream outputFile = null;
		BufferedOutputStream outputBuffer = null;

		inputStream = new BufferedInputStream(url.openStream());
		outputFile = new FileOutputStream(configuration.getTorrentFolder()
				+ "/" + filename);
		outputBuffer = new BufferedOutputStream(outputFile, 1024);
		byte data[] = new byte[1024];
		int bytesRead = 0;
		while ((bytesRead = inputStream.read(data, 0, 1024)) >= 0) {
			outputBuffer.write(data, 0, bytesRead);
		}

		// cleanup
		outputBuffer.close();
		inputStream.close();
	}

	/**
	 * Downloads and parses the feed at the given url. Returns an empty list on
	 * error, throws IOException if the file cannot be downloaded
	 * 
	 * @param url
	 *            the url of the feed to download and parse
	 * @throws IOException
	 *             when the download fails, often because of lack of Internet
	 *             connectivity
	 */
	private ArrayList<String> downloadTorrentList(String url)
			throws IOException {

		ArrayList<String> torrents = new ArrayList<String>();
		DocumentBuilder builder = null;

		try {
			// setup and download the feed
			DocumentBuilderFactory domFactory = DocumentBuilderFactory
					.newInstance();
			domFactory.setNamespaceAware(true);

			builder = domFactory.newDocumentBuilder();
			Document doc = builder.parse(url);

			// extract the torrent list with XPath
			XPathFactory factory = XPathFactory.newInstance();
			XPath xpath = factory.newXPath();
			XPathExpression expr = xpath
					.compile("//rss/channel/item/enclosure/@url");
			Object result = expr.evaluate(doc, XPathConstants.NODESET);
			NodeList nodes = (NodeList) result;

			// compile the torrent list
			for (int i = 0; i < nodes.getLength(); i++) {
				torrents.add(nodes.item(i).getNodeValue());
			}

		} catch (Exception e) {
			// no errors should happen unless there's problems with the Internet
			// connection. quietly notify user

			// grab the stack trace as string
			Writer result = new StringWriter();
			PrintWriter printWriter = new PrintWriter(result);
			e.printStackTrace(printWriter);
			String stackTrace = result.toString();

			// add a "problem"
			configuration.addProblem(new Problem("Couldn't parse your feed",
					stackTrace));
		}

		return torrents;
	}

}
